﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data.Common;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GsmComm.GsmCommunication;
using HardwareUtils;
using Lap.Collections;
using Lap.Extensions.Core;
using PpSmsSender.Db;

namespace PpSmsSender
{
    public interface IDbSmsSender
    {
        SmsSender Sender { get; }
        IConfiguration Config { get; }
        void WakeupMainthread();
        void ExecAdminCommand(string command, Action<string> func);
    }

    public class DbSmsSender : IDisposable, IDbSmsSender
    {
        private readonly PollingThread _mainthread;
        private readonly PollingThread _replyThread;
        public readonly SmsSender _sender;
        private WebServer _http;
        private DictionaryWithDefault<string, LoglineAddedEventHandler> _logginghandlers = new DictionaryWithDefault<string, LoglineAddedEventHandler>(k => null);
        private int _phoneix;
        private List<string> _phoneList = new List<string>();
        private Action<int> _action;
        private Task droptask;

        public DbSmsSender(Type phoneNumberType, string configFile, Action<string> debugOutput)
        {
            PhoneNumber.FactoryPhoneNumberType = phoneNumberType;
            this._sender = new SmsSender(this.RecieveMessageCallback, configFile, debugOutput);
            this._sender.FailedMessageCallback = this.HandleFailedMessageCallback;
            this._sender.SentMessageCallback = this.HandleSentMessageCallback;
            this._sender.RecieveStatusCallback = this.HandleRecieveStatusCallback;
            this._sender.QueueBackCallback = this.HandleQueueBackCallback;


            string httpPattern = ConfigurationManager.AppSettings["httpPattern"];
            var removepattern = httpPattern.Replace("https", "http");
            ShellExe shell = ShellExe.ExecuteProgram("netsh", "http delete urlacl url=" + removepattern);
            removepattern = httpPattern.Replace("http", "https");
            shell = ShellExe.ExecuteProgram("netsh", "http delete urlacl url=" + removepattern);

            shell = ShellExe.ExecuteProgram("netsh", "http add urlacl url=" + httpPattern + " user=Everyone");
            this._sender.OutputLn(shell.StdErr);


            this._sender.InitModemList();
            this._sender.Start();


            this._mainthread = new PollingThread(
                thread =>
                {
                    this.EventLoop();
                    return false;
                }, "DbSMSSenderMain", TimeSpan.FromSeconds(60));

            using (SmsDataContext db = new SmsDataContext())
            {
                var toSend = db.SmsMessages.Where(m => m.Typ == (int)SmsMessage.Types.Queued).Take(100).ToList();
                db.SaveChanges();
                toSend.ForEach(sms => this._sender.SendSms(sms));
            }

            this._mainthread.Start();
            this._http = new WebServer(this);
            this._mainthread.Cleanup = this._http.Stop;

            this._replyThread = new PollingThread(
                thread =>
                {
                    if (this._http.WebDied)
                    {
                        this._http.Dispose();
                        this._http = new WebServer(this);
                        this._http.Start(httpPattern, this._http.Webhandler);
                    }
                    this.ProcessReplies();
                    this.ProcessAck();
                    return false;
                }, "SendReplyThread", TimeSpan.FromSeconds(30));
            this._replyThread.Start();

            this._http.Start(httpPattern, this._http.Webhandler);

            _phoneList = _sender.Modems.Select(m => m.Value.Config.PhoneNumber.Number).ToList();

        }


        public IConfiguration Config
        {
            get { return _sender.ConfigRead; }
        }

        public void WakeupMainthread()
        {
            this.Mainthread.Resume();
        }

        public void ExecAdminCommand(string cmd, Action<string> writeLine)
        {
            cmd = cmd.ToLower();
            if (cmd.StartsWith("test "))
            {
                var trimStart = cmd.TrimStart("test").Trim().Split(' ');
                var testcnt = trimStart[0].Parse(1);
                int prio = trimStart.Length > 1 ? trimStart[1].Parse(8) : 8;
                this._action = i => this._sender.SendSms("TestMessage", _phoneList[++this._phoneix % _phoneList.Count], prio: prio);
                Range.Repeat(testcnt)
                    .ForEach(this._action);
            }

            if (cmd.StartsWith("modemlist"))
            {
                foreach (var m in this.Sender.Modems)
                {
                    writeLine("Port: " + m.Value.ComPort
                              + "    connected:" + (m.Value.ModemHandler.Connected ? "Yes" : "No")
                              + "    recieveOnly:" + (m.Value.ModemHandler.IsReciveOnly ? "Yes" : "No")
                              + "    paused:" + (m.Value.Config.IsPaused ? "Yes" : "No")
                        );
                    writeLine("  Name     : " + m.Value.DevInfo.Description);
                    writeLine("  DeviceID : " + m.Value.DevInfo.DeviceID);
                }
            }

            if (cmd.StartsWith("pause "))
            {
                cmd = cmd.TrimStart("pause ").ToUpper().Trim();
                this.Sender.Modems.Where(x => x.Key.ToUpper() == cmd || cmd == "ALL")
                    .Select(x => x.Value)
                    .ForEach(m => m.Config.IsPaused = true);
                writeLine("Paused " + cmd);
            }

            if (cmd.StartsWith("unpause "))
            {
                cmd = cmd.TrimStart("unpause ").ToUpper().Trim();
                this.Sender.Modems.Where(x => x.Key.ToUpper() == cmd || cmd == "ALL")
                    .Select(x => x.Value)
                    .ForEach(m => m.Config.IsPaused = false);
                writeLine("Unpaused " + cmd);
            }

            if (cmd.StartsWith("reconnect "))
            {
                foreach (var m in this.Sender.Modems)
                {
                    if (m.Value.ModemHandler.Connected == false)
                    {
                        this.Sender.Modems[m.Key].ModemHandler.ConnectModem(m.Key);
                    }
                }
            }


            if (cmd.StartsWith("reboot"))
            {
                Process.Start("shutdown", "/r /t 10");
                writeLine("Reboot command recieved, will reboot in 10 seconds");
            }

            if (cmd.StartsWith("cycle "))
            {
                cmd = cmd.TrimStart("cycle ");
                string port = cmd.ToUpper().Trim();
                this.droptask = Task.Factory.StartNew(() =>
                {

                    ShellExe res;
                    foreach (var m in this.Sender.Modems.Where(x => x.Key.ToUpper() == port || port == "ALL")
                        .Select(x => x.Value))
                    {
                        m.ModemHandler.DropModem();

                    }
                    Thread.Sleep(5000);
                    foreach (var m in this.Sender.Modems.Where(x => x.Key.ToUpper() == port || port == "ALL")
                        .Select(x => x.Value))
                    {
                        res = DevCon.RemoveDevice(m.DevInfo.DeviceID);
                        writeLine(res.StdOut);
                        writeLine(res.StdErr);
                    }
                    Thread.Sleep(30000);
                    res = DevCon.RescanDevices();
                    writeLine(res.StdOut);
                    writeLine(res.StdErr);
                    foreach (var m in this.Sender.Modems.Where(x => x.Key.ToUpper() == port || port == "ALL")
                        .Select(x => x.Value))
                    {
                        m.ModemHandler.DropModem();
                        this.Sender.ConnectModem(m);
                    }
                    droptask = null;
                });
            }

            if (cmd.StartsWith("restart "))
            {
                cmd = cmd.TrimStart("restart ");
                string port = cmd.ToUpper().Trim();
                foreach (var m in this.Sender.Modems.Where(x => x.Key.ToUpper() == port || port == "ALL").Select(x => x.Value))
                {
                    ShellExe res = DevCon.RestartDevice(m.DevInfo.DeviceID);
                    writeLine(res.StdOut);
                    writeLine(res.StdErr);
                }
            }

            if (cmd.StartsWith("status "))
            {
                cmd = cmd.TrimStart("status ");
                string port = cmd.ToUpper().Trim();
                foreach (var m in this.Sender.Modems.Where(x => x.Key.ToUpper() == port || port == "ALL").Select(x => x.Value))
                {
                    try
                    {
                        // Get signal quality
                        SignalQualityInfo info = m.ModemHandler._comm.GetSignalQuality();
                        writeLine("Signal strength: " + info.SignalStrength.ToString());
                        writeLine("Bit error rate: " + info.BitErrorRate.ToString());
                        OperatorInfo opInfo = m.ModemHandler._comm.GetCurrentOperator();
                        if (opInfo != null)
                        {
                            writeLine("Operator info:");
                            writeLine("  Format: " + opInfo.Format);
                            writeLine("  Operator: " + opInfo.TheOperator);
                            writeLine("  Access Technology: " + opInfo.AccessTechnology);
                        }
                        else
                        {
                            writeLine("There is currently no operator selected!");
                        }
                    }
                    catch (Exception ex)
                    {
                        writeLine(ex.Message);
                    }

                }
            }

            if (cmd.StartsWith("startlog "))
            {
                cmd = cmd.TrimStart("startlog ");
                string port = cmd.ToUpper().Trim();
                foreach (var m in this.Sender.Modems.Where(x => x.Key.ToUpper() == port || port == "ALL").Select(x => x.Value))
                {
                    try
                    {
                        LoglineAddedEventHandler commOnLoglineAdded = (sender, args) => writeLine(args.Text);
                        if (this._logginghandlers[m.ComPort.ToUpper()] != null)
                            m.ModemHandler._comm.LoglineAdded -= this._logginghandlers[m.ComPort.ToUpper()];
                        this._logginghandlers[m.ComPort.ToUpper()] = commOnLoglineAdded;
                        m.ModemHandler._comm.LoglineAdded += this._logginghandlers[m.ComPort.ToUpper()];
                    }
                    catch (Exception ex)
                    {
                        writeLine(ex.Message);
                    }

                }
            }

            if (cmd.StartsWith("stoplog "))
            {
                cmd = cmd.TrimStart("stoplog ");
                string port = cmd.ToUpper().Trim();
                foreach (var m in this.Sender.Modems.Where(x => x.Key.ToUpper() == port || port == "ALL").Select(x => x.Value))
                {
                    try
                    {
                        if (this._logginghandlers[m.ComPort.ToUpper()] != null)
                            m.ModemHandler._comm.LoglineAdded -= this._logginghandlers[m.ComPort.ToUpper()];
                    }
                    catch (Exception ex)
                    {
                        writeLine(ex.Message);
                    }
                }
            }
        }

        private void ProcessAck()
        {
            try
            {
                string ackUrl = ConfigurationManager.AppSettings["callbackAck"];
                if (!ackUrl.IsNullOrWhiteSpace())
                {
                    using (SmsDataContext db = new SmsDataContext())
                    {
                        StringBuilder sb = new StringBuilder();
                        DateTime minus1Hour = DateTime.Now.AddHours(-1);
                        var toSend = db.SmsMessages
                            .Where(m =>
                                   (m.Typ == (int)SmsMessage.Types.Sent || m.Typ == (int)SmsMessage.Types.FailedSend)
                                   && m.Callbackid != null && m.Callbackid != ""
                                   && (m.Status == (int)SmsMessage.Statuses.SendDone || (m.RecievedAt.HasValue && m.RecievedAt < minus1Hour)))
                            .OrderBy(m => m.SentAt)
                            .Take(100).ToList();
                        if (toSend.Count > 0)
                        {
                            toSend.ForEach(m =>
                                               {
                                                   sb.AppendFormat("&msgId={0}", m.Id.ToString().UrlEncode(Encoding.UTF8));
                                                   sb.AppendFormat("&callbackid={0}", m.Callbackid.ToString().UrlEncode(Encoding.UTF8));
                                                   sb.AppendFormat("&time={0}", m.SentAt.GetValueOrDefault().ToString("yyyy-MM-dd HH:mm:ss").UrlEncode(Encoding.UTF8));
                                                   sb.AppendFormat("&status={0}", m.Status.ToString().UrlEncode(Encoding.UTF8));
                                                   sb.AppendFormat("&tonumber={0}", m.Number_Number.UrlEncode(Encoding.UTF8));
                                                   sb.AppendFormat("&bynumber={0}", m.RecievedBy_Number.UrlEncode(Encoding.UTF8));

                                                   m.Type = SmsMessage.Types.AckedSent;
                                               });
                            string postData = sb.ToString();
                            HttpSender.Post(ackUrl, postData, Encoding.UTF8);
                            db.SaveChanges();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                this.Sender.OutputLn("Failure in DbSmsSender.ProcessAck: " + ex.Message);
            }
        }

        private void ProcessReplies()
        {
            try
            {
                string recUrl = ConfigurationManager.AppSettings["callbackRec"];
                if (!recUrl.IsNullOrWhiteSpace())
                {
                    using (SmsDataContext db = new SmsDataContext())
                    {
                        StringBuilder sb = new StringBuilder();
                        sb.Append("x=0");//Something to start with
                        var toSend = db.SmsMessages
                            .Where(m =>
                                   m.Typ == (int)SmsMessage.Types.Recieved)
                            .OrderBy(m => m.RecievedAt)
                            .Take(100).ToList();
                        if (toSend.Count > 0)
                        {
                            toSend.ForEach(
                                m =>
                                {
                                    if (!_phoneList.Contains(m.Number_Number))
                                    {
                                        sb.AppendFormat("&msgId={0}", m.Id.ToString().UrlEncode(Encoding.UTF8));
                                        sb.AppendFormat("&fromnumber={0}", m.Number_Number.UrlEncode(Encoding.UTF8));
                                        sb.AppendFormat("&msg={0}", m.Text.UrlEncode(Encoding.UTF8));
                                        sb.AppendFormat("&time={0}", m.RecievedAt.GetValueOrDefault().ToString("yyyy-MM-dd HH:mm:ss").UrlEncode(Encoding.UTF8));
                                        sb.AppendFormat("&bynumber={0}", m.RecievedBy_Number.UrlEncode(Encoding.UTF8));
                                    }
                                    m.Type = SmsMessage.Types.ForwardedRecieved;
                                });

                            string postData = sb.ToString();
                            if (postData != "x=0")
                                HttpSender.Post(recUrl, postData, Encoding.UTF8);

                            db.SaveChanges();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                this.Sender.OutputLn("Failure in DbSmsSender.ProcessReplies: " + ex.Message);
            }
        }

        private void EventLoop()
        {
            if (this._sender.SendQueueLength < 20)
            {
                using (SmsDataContext db = new SmsDataContext())
                {
                    var toSend = db.SmsMessages
                        .Where(m => m.Typ == (int)SmsMessage.Types.ToSend
                                    && (m.EarliestSend == null || m.EarliestSend < DateTime.Now))
                        .OrderBy(m => m.Priority)
                        .Take(100)
                        .ToList();

                    toSend
                        .ForEach(
                            sms =>
                            {
                                if (!this.Sender.ConfigRead.IsSendAlllowed(sms.Priority, DateTime.Now.AddMinutes(5)))
                                {
                                    sms.EarliestSend = DateTime.Now.AddMinutes(10);
                                }
                                else
                                {
                                    sms.Type = SmsMessage.Types.Queued;
                                }
                            });
                    db.SaveChanges();
                    toSend
                        .Where(sms => sms.Type == SmsMessage.Types.Queued)
                        .ForEach(sms => this._sender.SendSms(sms));
                }
            }
        }

        private void HandleRecieveStatusCallback(RecievedMessageStatus obj)
        {
            using (SmsDataContext db = new SmsDataContext())
            {
                DateTime twodaysback = DateTime.Now.AddDays(-2);
                SmsMessage msg = db.SmsMessages.FirstOrDefault(m =>
                                                               obj.Number.Number == m.Number_Number
                                                               && obj.Refbyte == m.Reference
                                                               && m.SentAt.Value > twodaysback);
                if (msg != null)
                {
                    msg.Type = SmsMessage.Types.Sent;
                    msg.Status = obj.Status;
                    msg.Result += "\r\nRecieved status " + obj.RecievedAt.ToString("yyy-MM-dd HH:mm:ss") + " Status=" + obj.Status.ToString();
                    db.SaveChanges();
                }
                else
                {
                    this.Sender.OutputLn("\r\nNot found: Recieved status for " + obj.Number.Number + "(" + obj.Refbyte + ") at " + obj.RecievedAt.ToString("yyy-MM-dd HH:mm:ss") + " Status=" + obj.Status.ToString());
                }
            }
        }


        private void HandleSentMessageCallback(SmsMessage msg)
        {
            using (SmsDataContext db = new SmsDataContext())
            {
                msg.InsertOrAttach(db);
                msg.Type = SmsMessage.Types.Sent;
                db.SaveChanges();
            }
        }

        private void HandleQueueBackCallback(SmsMessage msg)
        {
            using (SmsDataContext db = new SmsDataContext())
            {
                msg.InsertOrAttach(db);
                msg.Type = SmsMessage.Types.ToSend;
                db.SaveChanges();
            }
        }

        private void HandleFailedMessageCallback(SmsMessage msg)
        {
            using (SmsDataContext db = new SmsDataContext())
            {
                msg.InsertOrAttach(db);
                msg.Type = SmsMessage.Types.FailedSend;
                db.SaveChanges();
            }
        }

        private void RecieveMessageCallback(SmsMessage msg)
        {
            using (SmsDataContext db = new SmsDataContext())
            {
                msg.InsertOrAttach(db);
                // msg.Type = SmsMessage.Types.Recieved;
                db.SaveChanges();
            }
        }

        public void SendSms(string text, string reciever, string port = null, string sendingnumber = null, int prio = 8, string callbackid = null)
        {
            this._sender.SendSms(text, reciever, port, sendingnumber, prio);
        }

        #region Implementation of IDisposable
        /// <summary>
        ///     Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            if (droptask != null && !droptask.IsCompleted)
                droptask.Wait();
            this._http.Dispose();
            this._mainthread.TryStop();
            this._replyThread.TryStop();
            this._sender.Dispose();
        }
        #endregion

        #region Implementation of IDbSmsSender
        public PollingThread Mainthread
        {
            get { return this._mainthread; }
        }

        public SmsSender Sender
        {
            get { return this._sender; }
        }
        #endregion
    }

    public static class AppsettingExtension
    {
        public static string ConfItem(this string appSetting, string userID)
        {
            try
            {
                DbConnectionStringBuilder csb = new DbConnectionStringBuilder();
                csb.ConnectionString = appSetting;
                return csb[userID] as string;
            }
            catch (Exception)
            {
                return "";
            }
        }
    }
}